#!/usr/bin/env python
# -*- coding: UTF-8 -*-

#  pyotp.py
#  
#  Copyright 2011 Alexander Lebedinsky <pr0f.a7ex@gmail.com>
#  
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#  
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#  
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.

import sys
import os
import serial


def get_byte(char):
    '''Return char as int. For str to int conversion'''
    return int(char[0].encode('hex_codec'), 16) & 0xFF

fontA, fontB, fontC = ('A', 'B', 'C')

# barcode types
UPC_A   = 65
UPC_E   = 66
EAN13   = 67
EAN8    = 68
CODE39  = 69
ITF     = 70
CODABAR = 71
CODE93  = 72
CODE128 = 73

# aligment
LEFT_ALIGM, CENTER_ALIGM, RIGHT_ALIGM = (0, 1, 2)

CMD_OK          = 0
E_PORT_NOT_OPEN = -1
E_PORT_OPENED   = -2
E_NO_ANSWER     = -3
E_COVER_OPEN    = -4
E_FEED_BTN      = -5
E_PAPER_OUT     = -6
E_ERROR_OCCURED = -7
E_MECHANICAL    = -8
E_AUTOCUTTER    = -9
E_FATAL_ERROR   = -10
E_RECOVER_ERROR = -11
E_PAPER_NEAR_END = -12
E_PAPER_NOT_PRES = -13


class Printer:
    serial_port = None
    url         = None
    port_opened = False
    _buffer     = bytearray()
    font_size   = (12, 24)  # Font A (12, 24), Font B (9, 24) or (9, 17)
    font_mul    = (1, 1)
    maker       = ''
    model       = ''
    model_id    = ''

    # Port parameters
    port_params = {
            # 'port' : None, # not need
            'do_not_open': 1,
            'baudrate': 19200,
            'bytesize': serial.EIGHTBITS,
            'parity': serial.PARITY_NONE,
            'stopbits': serial.STOPBITS_ONE,
            'timeout': 0.1,
            'writeTimeout': None,
            'xonxoff': False,
            'rtscts': False,
            'dsrdtr': False,
            'interCharTimeout': None
            }

    def __init__(self, **kwargs):
        self.set_port_params(**kwargs)
        self.set_port_url(self.url)

    def __open_port__(self):
        if self.serial_port is None:
            sys.stderr.write('Set port parameters first!\n')
            sys.exit(1)

        try:
            self.serial_port.open()
        except serial.SerialException, e:
            sys.stderr.write('Could not open serial port %s: %s\n' %
                             (self.serial_port.portstr, e))
            sys.exit(1)
        self.port_opened = True
    
    def close_port(self, p_flush_buffer = True):
        if not self.port_opened:
            return E_PORT_NOT_OPEN
        
        if not self.buffer_is_empty() and p_flush_buffer:
            self.flush_buffer()
        else:
            self.clear_buffer()
        
        self.serial_port.close()
        self.port_opened = False
    
    def set_port_url(self, url):
        ''' Port url sets a new value. Port will be reopened , if necessary '''
        if url == self.url:
            # nothing to change
            return CMD_OK

        port_state = self.port_opened
        if port_state:
            self.close_port(False)

        self.url = url
        self.serial_port = serial.serial_for_url(self.url, **self.port_params)
        if port_state:
            self._open_port()
        return CMD_OK

    def set_port_params(self, **kwargs):
        if self.port_opened:
            # port opened, exit
            return E_PORT_OPENED

        if os.name == 'posix':
            self.url = kwargs.get('url', '/dev/ttyS0')
        elif os.name == 'nt':
            self.url = kwargs.get('url', 'COM1')

        for i in self.port_params.keys():
            if i == 'do_not_open':
                self.port_params[i] = kwargs.get( i, self.port_params[i])
            elif i == 'timeout':
                timeout = kwargs.get('timeout',0.1)
                if timeout < 0.01: timeout = 0.1
                self.port_params['timeout'] = timeout
            elif i == 'baudrate':
                baudrate = kwargs.get('baudrate', 0)
                # Вроде принтер по другому не работает
                if not baudrate == 19200:
                    baudrate = 19200
                self.port_params['baudrate'] = baudrate
            
            self.port_params[i] = kwargs.get( i, self.port_params[i])

    def buffer_is_empty(self):
        return not len(self._buffer)

    def write_bytes(self, data):
        if not self.port_opened:
            return E_PORT_NOT_OPEN
        self.serial_port.write(data)
        return CMD_OK

    def write_and_read(self, data, n_bytes=1, timeout=0.1):
        self.serial_port.write(data)
        if timeout < 0.01:
            timeout=0.01
        self.serial_port._timeout = timeout
        
        try:
            ret = self.serial_port.read(n_bytes)
        except:
            ret = ''
        return ret

    def clear_buffer(self):
        ''' Clear buffer. All not sended data lost! '''
        self._buffer = bytearray()

    def put_in_buffer(self, data):
        for item in data:
            self._buffer.append(item)

    def flush_buffer(self, flush = True):
        ''' Flush & clear buffer '''
        if self.buffer_is_empty():
            return CMD_OK

        if flush and not self._buffer[:-1] == '\n': # TODO: extend flush symbol list
            self.put_in_buffer('\n')
        ret = self.write_bytes(self._buffer)
        if ret == CMD_OK:
            self.clear_buffer()
        return ret

    def set_character_font(self, font):
        ''' Select character font '''
        # ASCII ESC M n
        # Hex.  1B 4D n
        # Dec.  27 77 n
        # n = 0,48 Selects Font A (12 x 24).
        # n = 1,49 Selects Font B (9 x 17).
        if font == fontA:
            self.put_in_buffer([ 27, 77, 0])
            self.font_size = ( 12 * self.font_mul[0], 24 * self.font_mul[1])
        elif font == fontB:
            self.put_in_buffer([ 27, 77, 1])
            self.font_size = ( 9 * self.font_mul[0], 24 * self.font_mul[1])
        elif font == fontC:
            self.put_in_buffer([ 27, 77, 2])
            self.font_size = ( 9 * self.font_mul[0], 24 * self.font_mul[1])

    def set_font_mag(self, mag_x, mag_y):
        mul_x = mag_x & 7
        mul_y = mag_y & 7
        # Select character size
        # ASCII GS ! n
        # Hex.  1D 21 n
        # Dec.  29 33 n
        self.put_in_buffer([ 29, 33, mul_x | mul_y << 4 ])
        self.font_mul = (mul_x+1, mul_y+1)
        self.font_size = (self.font_size[0] * (mul_x+1), 
							self.font_size[1] * (mul_y+1))

    def __get_model__(self):
        if not self.port_opened:
            self.__open_port__()

        if not self.buffer_is_empty():
            # buffer must be empty
            self.clear_buffer()
        
        mod_id = self.write_and_read(serial.to_bytes([0x1D,0x49,0x01]),1)
        if len(mod_id) == 0:
            self.model_id = '0x?? (ERROR: MODEL CODE NOT RESIVED)'
            return E_NO_ANSWER
        mod_id = get_byte(mod_id[0])
        if mod_id == 0x30:
            self.maker      = 'CITIZEN (expected on ModelID)'
            self.model      = 'CBM1000II (expected on ModelID)'
            self.model_id   = '0x30 (CITIZEN CBM1000II)'
        elif mod_id == 0x20:
            self.maker      = 'EPSON (expected on ModelID)'
            self.model      = 'TM-T88II (expected on ModelID)'
            self.model_id   = '0x20 (EPSON TM-T88II)'
        elif mod_id == 0x27:
            self.maker      = 'EPSON (expected on ModelID)'
            self.model      = 'BA-T500 (expected on ModelID)'
            self.model_id   = '0x27 (EPSON BA-T500)'

        #ba[bytes++]=0x1D; // [GS]
        #ba[bytes++]=0x49; // I
        #ba[bytes++]=66; // maker   строкой
        # на некоторых принтерах может не поддерживаться
        maker_str = self.write_and_read(serial.to_bytes([0x1D,0x49,66]),20)
        if len(maker_str):
            self.maker = maker_str

        # ba[bytes++]=0x1D; // [GS]
        # ba[bytes++]=0x49; // I
        # ba[bytes++]=67; // model  строкой
        # на некоторых принтерах может не поддерживаться
        mod_str = self.write_and_read(serial.to_bytes([0x1D,0x49,67]),20)
        if len(mod_str):
            self.model = mod_str

    def get_device_status(self):
        if not self.port_opened:
            return E_PORT_NOT_OPEN

        state = self.write_and_read(serial.to_bytes([0x10,0x04, 1]),1)
        if len(state) == 0:
            return E_NO_ANSWER

        state_int = get_byte(state[0])
        if state_int & 0x08 > 0: # offline
            state = self.write_and_read(serial.to_bytes([0x10,0x04, 2]),1)
            if not len(state):
                return E_NO_ANSWER
            state_int = get_byte(state[0])
            if state_int & 0x04 > 0:
                # cover open
                return E_COVER_OPEN
            if state_int & 0x08 > 0:
                # Paper is being feed by the paper feed button.
                return E_FEED_BTN
            if state_int & 0x20 > 0:
                # Paper out.
                return E_PAPER_OUT
            if state_int & 0x40 > 0:
                # Error occurred.
                return E_ERROR_OCCURED

        # 03 - Transmit error status
        state = self.write_and_read(serial.to_bytes([0x10,0x04, 3]),1)
        # Error status (n = 3) is as follows:
        # Bit  Binary    Hex     Decimal        Status
        # 0    0         00      0              Not used. Fixed to Off.
        # 1    1         02      2              Not used. Fixed to On.
        # 2    0         00      0              No Mechanical Error.
        #      1         04      4              Mechanical Error occurred.
        # 3    0         00      0              No autocutter error.
        #      1         08      8              Autocutter error occurred.
        # 4    1         10      16             Not used. Fixed to On.
        # 5    0         00      0              No unrecoverable error.
        #      1         20      32             Unrecoverable error occurred.
        # 6    0         00      0              No auto-recoverable error.
        #      1         40      64             Auto-recoverable error occurred.
        # 7    0         00      0              Not used. Fixed to Off.
        if len(state) == 0:
            return E_NO_ANSWER
        state_int = get_byte(state[0])
        if state_int & 0x04 > 0:
            # Mechanical error occurred.
            return E_MECHANICAL
        if state_int & 0x08 > 0:
            # Autocutter error occurred.
            return E_AUTOCUTTER
        if state_int & 0x20 > 0:
            # No recoverable error.
            return E_FATAL_ERROR
        if state_int & 0x40 > 0:
            # Recoverable error.
            return E_RECOVER_ERROR

        # 04 - Transmit roll paper sensor status
        state = self.write_and_read(serial.to_bytes([0x10,0x04, 4]),1)
        # Roll paper sensor status (n = 4) is as follows:
        # Bit  Off/On     Hex     Decimal       Status
        # 0    0          00      0             Not used. Fixed to Off.
        # 1    1          02      2             Not used. Fixed to On.
        # 2, 3 00         00      0             Roll paper near-end sensor: paper adequate.
        #      11         0C      12            Roll paper near-end sensor: paper near end.
        # 
        # 4    1          10      16            Not used. Fixed to On.
        # 5, 6 00         00      0             Roll paper end sensor: paper present.
        #      11         60      96            Roll paper end sensor: paper not present.
        #
        # 7    0          00      0             Not used. Fixed to Off.
        if len(state) == 0:
            return E_NO_ANSWER
        state_int = get_byte(state[0])
        if state_int & 0x0C > 0:
            # paper near end.
            return E_PAPER_NEAR_END
        if state_int & 0x60 > 0:
            # paper not present.
            return E_PAPER_NOT_PRES

        return CMD_OK

    def init_device(self):
        if not self.port_opened:
            self.__open_port__()

        # Initialize printer
        # ASCII ESC @
        # Hex 1B 40
        # Decimal 27 64
        self.write_bytes(serial.to_bytes([27,64]))
        self.clear_buffer()
        
        self.__get_model__()

        return self.get_device_status()

    def set_bold(self, bold = True):
        ''' Specify/cancel double printing '''
        # ASCII     ESC G n
        # Hex.      1B 47 n
        # Decimal   27 71 n
        if bold:
            self.put_in_buffer([0x1B,71,1])
        else:
            self.put_in_buffer([0x1B,71,0])

    def set_negative(self, negative = True):
        ''' Specify/cancel white/black inverted printing '''
        # ASCII     GS B n
        # Hex.      1D 42 n
        # Decimal   29 66 n
        if negative:
            self.put_in_buffer([0x1D,66,1])
        else:
            self.put_in_buffer([0x1D,66,0])

    def set_underline(self, underline = True, double = False):
        ''' Specify/cancels underline mode '''
        # ASCII     ESC _ n
        # Hex.      1B 2D n
        # Decimal   27 45 n
        # 0, 48 Cancels underline
        # 1, 49 Sets to one-dot width underline and specifies underlines.
        # 2, 50 Sets to two-dot width underline and specifies underlines.
        if underline:
            if double:
                self.put_in_buffer([0x1B,45,2])
            else:
                self.put_in_buffer([0x1B,45,1])
        else:
            self.put_in_buffer([0x1B,45,0])

    def set_aligment(self, aligment = LEFT_ALIGM):
        ''' Position alignment '''
        # ASCII ESC a n
        # Hex.  1B 61 n
        # Dec.  27 97 n
        if not self.buffer_is_empty() and not self._buffer[:-1] == '\n':
            # without LF not usefull
            self.put_in_buffer([0x0A])
        self.put_in_buffer([0x1B,0x61,aligment])

    def cut_paper(self, full = False, feed = False):
        ''' Cut paper '''
        # Set line feed amount
        # ASCII ESC 3 n
        # Hex.  1B 33 n
        # Dec.  27 51 n
        if feed:
            self.put_in_buffer([0x1B, 0x33, 20, 0x0A])
        else:
            self.put_in_buffer([0x1B, 0x33, 0, 0x0A])
        
        # ASCII GS V m n
        # Hex.  1D 56 m n
        # Dec.  29 86 m n
        # 65 - feed & full cut
        # 66 - feed & partial cut
        if full:
            #if feed:
            self.put_in_buffer([0x1D, 0x56, 65]) # not work on Posiflex
        #    else:
        #        self.put_in_buffer([0x1D, 0x56, 0, 0])
        else:
            #if feed:
            self.put_in_buffer([0x1D, 0x56, 66])
        #    else:
        #        self.put_in_buffer([0x1D, 0x56, 1, 0])

    def do_beep(self):
        ''' Do beep '''
        #TmpBA[bytes++]=0x1D ; //не ESC-POS команда ? - видимо только _PP5200_!! (pp7000II ok)
        #TmpBA[bytes++]=0x6f ;
        #TmpBA[bytes++]=0x01 ;
        #TmpBA[bytes++]=0x1b ;
        #TmpBA[bytes++]=0x6f ;
        self.put_in_buffer([0x1D, 0x6F, 0x01, 0x1B, 0x6F])

    def put_text(self, text):
        ''' Put simple text in buffer '''
        # Select character code table
        # ASCII ESC t n
        # Hex.  1B 74 n
        # Dec.  27 116 n
        # нас интересует 17(0x11) Page 17 [PC866 (Cyrillic #2)]
        self.put_in_buffer([0x1B,0x74,0x11])

        # Select an international character set
        # ASCII ESC R  n
        # Hex   1B 52  n
        # Dec.  27 82  n
        # выставим это в 0.
        # а если не 0 - то меняется вид символов : #$@[\]^`{|}
        # prof-alex: 0 - America
        self.put_in_buffer([0x1B,0x52,0])

        encoded_text = ''
        try:
            encoded_text = text.encode('cp866', 'replace')
        except:
            # another try
            try:
                encoded_text = unicode(text,'cp1251', 'replace').encode('cp866', 'replace')
            except:
                pass
        #self.put_in_buffer(text.encode('cp866', 'replace'))
        self.put_in_buffer(encoded_text)

    def draw_barcode(self, code, code_type = EAN13, print_char = 2, height = 86, width = 3):
        ''' Print bar code '''
        if not self.buffer_is_empty() and not self._buffer[:-1] == '\n':
            # We lost all data if not separate by LF
            self.put_in_buffer([0x0A])

        # fist set width & height
        if width > 6:
            width = 6
        elif width < 1:
            width = 1
        # Set bar code horizontal size
        # ASCII GS w n
        # Hex.  1D 77 n
        # Dec.  29 119 n
        self.put_in_buffer([0x1D,0x77,width])

        # Set bar code height
        # ASCII GS h n
        # Hex.  1D 68 n
        # Dec.  29 104 n
        height &= 0xFF
        self.put_in_buffer([0x1D,0x68,height])

        # Select HRI character print position
        # ASCII GS H n
        # Hex. 1D 48 n
        # Dec. 29 72 n
        # 0, 48 - No print
        # 1, 49 - Above bar code
        # 2, 50 - Below bar code
        # 3, 51 - Above and below bar code (both)
        if print_char < 0:
            print_char = 0
        elif print_char > 51:
            print_char = 51
        self.put_in_buffer([0x1D,0x48,print_char])

        # Code 1.
        # ASCII     GS k    m d1...dk NUL
        # Hex.      1D 6B   m d1...dk NUL
        # Decimal   29 107  m d1...dk NUL
        # 2.
        # ASCII     GS k    m n d1...dk
        # Hex.      1D 6B   m n d1...dk
        # Decimal   29 107  m n d1...dk
        self.put_in_buffer([0x1D,0x6B, code_type, len(code)])
        self.put_in_buffer([k for k in code.encode('cp866', 'replace')])

    def put_image():
        # Попробуем картинку конвертировать в B/W, а затем перевернуть и пихнуть в массив
        # im.convert(mode) => image
        # Returns a converted copy of an image.
        # When converting to a bilevel image (mode "1"), the source image
        # is first converted to black and white. Resulting values larger
        # than 127 are then set to white, and the image is dithered.
        # To use other thresholds, use the point method.
        #
        # im.rotate(angle, filter=NEAREST, expand=0) => image
        # Returns a copy of an image rotated the given number of degrees
        # counter clockwise around its centre.
        # If the filter argument omitted, or if the image has mode "1"
        # or "P", it is set to NEAREST.
        # The expand argument, if true, indicates that the output image
        # should be made large enough to hold the rotated image.
        # If omitted or false, the output image has the same size as the input image. 
        pass

if __name__ == '__main__':
    #test = Printer(url='/dev/ttyUSB0', baudrate=19200)
    test = Printer()
    test.set_port_url('/dev/ttyUSB0')

    test.init_device()
    sys.stdout.write('model ID: %s\n' % test.model_id)
    sys.stdout.write('model: %s\n' % test.model)
    sys.stdout.write('maker: %s\n' % test.maker)
    sys.stdout.write('state: %i\n' % test.get_device_status())

    ## test and hard reset
    # Test print
    # ASCII GS (  A  pL pH n m
    # Hex.  1D 28 41 pL pH n m
    # Dec.  29 40 65 pL pH n m
    #
    # Defined Region {pL+ (pH×256) } = 2 (pL = 2,pH = 0)
    # n
    # 0, 48 - Basic sheet (paper roll)
    # 1, 49 - Paper Roll
    # 2, 50
    # m
    # 1, 49 - Hexadecimal Dump
    # 2, 50 - Printer Status (Self Print)
    # 3, 51 - Rolling Pattern Print
    # test.write_bytes(serial.to_bytes([0x1D,0x28,0x41,0x02,0x0,1,2]))
    # sys.exit(0)

    #test.put_text('1234567890'*5)

    test.put_text(u'Привет мир!\n')
    test.put_text(u'Тест штрихкода!')
    test.draw_barcode('123456789012')
    test.set_aligment(LEFT_ALIGM)
    test.put_text(u'Лево!')
    test.set_aligment(CENTER_ALIGM)
    test.put_text(u'Центр!')
    test.set_aligment(RIGHT_ALIGM)
    test.put_text(u'Право!')
    test.set_aligment(LEFT_ALIGM)

    test.set_bold()
    test.put_text(u'Жирный текст\n')
    test.set_bold(False)

    test.set_underline(True, False)
    test.put_text(u'Подчёркнутый 1\n')
    test.set_underline(True, True)
    test.put_text(u'Подчёркнутый 2 (должна быть двойная линия, но Posiflex её не понимает)\n')
    test.set_underline(False)

    test.set_negative(True)
    test.put_text(u'Негатив!\n')
    test.set_negative(False)

    test.set_font_mag(1, 1)
    test.put_text(u'В два раза больше!\n')
    test.set_font_mag(0, 0)
    
    test.set_aligment(CENTER_ALIGM)
    test.put_text(u'Аллес!\n')

    test.do_beep()
    test.cut_paper()
    #test.flush_buffer()
    test.close_port()
